package com.ncf.jucai.mybatis.generator;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.*;

import lombok.Data;
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
import org.mybatis.generator.api.*;
import org.mybatis.generator.api.dom.java.*;
import org.mybatis.generator.api.dom.xml.Attribute;
import org.mybatis.generator.api.dom.xml.Document;
import org.mybatis.generator.api.dom.xml.TextElement;
import org.mybatis.generator.api.dom.xml.XmlElement;
import org.mybatis.generator.codegen.RootClassInfo;
import org.mybatis.generator.codegen.mybatis3.model.BaseRecordGenerator;
import org.mybatis.generator.config.PropertyRegistry;
import org.mybatis.generator.config.TableConfiguration;
import org.mybatis.generator.internal.ObjectFactory;
import org.mybatis.generator.internal.db.ConnectionFactory;
import org.mybatis.generator.internal.db.DatabaseIntrospector;

import javax.annotation.PostConstruct;

@Data
public class ManyToOneRelationPlugin extends PluginAdapter {
    @Data
    static public class RelationContext {
        String columnName;
        String targetFieldName;
        String targetFieldClass;
        String targetTableName;
        IntrospectedTable targetTable;
    }

    private String tableName;
    private String modelName;
    private Connection connection;

    private Map<String, RelationContext> relationContexts = new HashMap<>();

    private Map<String, IntrospectedTable> tables = new HashMap<>();

    @Override
    public void setProperties(Properties properties) {
        List<String> warnings = new ArrayList<>();
        JavaTypeResolver javaTypeResolver = ObjectFactory .createJavaTypeResolver(context, warnings);
        DatabaseIntrospector databaseIntrospector = null;
        try {
            connection = ConnectionFactory.getInstance().getConnection(context.getJdbcConnectionConfiguration());
            databaseIntrospector = new DatabaseIntrospector(context, connection.getMetaData(), javaTypeResolver, warnings);
            TableConfiguration tableConfiguration = new TableConfiguration(context);
            tableConfiguration.setTableName(tableName);
            for (IntrospectedTable table: databaseIntrospector.introspectTables(new TableConfiguration(context))) {
                tables.put(table.getFullyQualifiedTable().getIntrospectedTableName(), table);
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
        super.setProperties(properties);
        setTableName(properties.getProperty("tableName"));
        setModelName(context.getJavaModelGeneratorConfiguration().getTargetPackage() + "." + properties.getProperty("modelName"));
        Enumeration names = properties.propertyNames();
        while (names.hasMoreElements()) {
            String name = (String) names.nextElement();
            if (name.endsWith("Field")) {
                String field = properties.getProperty(name);
                String[] segs = field.split(":");
                RelationContext relationContext = new RelationContext();
                relationContext.setColumnName(segs[0]);
                relationContext.setTargetFieldName(segs[1]);
                relationContext.setTargetFieldClass(segs[2]);
                relationContext.setTargetTableName(segs[3]);
                relationContexts.put(relationContext.getColumnName(), relationContext);
            }
        }
    }

    @Override
    public List<GeneratedJavaFile> contextGenerateAdditionalJavaFiles(IntrospectedTable introspectedTable) {
        List<GeneratedJavaFile> files = new ArrayList<>();
        if (hasRelation(introspectedTable)) {
            files.add(generateModelFileWithRelation(introspectedTable));
        }
        return files;
    }

    @Override
    public boolean clientSelectByPrimaryKeyMethodGenerated(Method method, Interface interfaze, IntrospectedTable introspectedTable) {
        if (hasRelation(introspectedTable)) {
            interfaze.addMethod(generateClientSelectByPrimaryKeyWithRelation(method, introspectedTable));
            Map<String, RelationContext> currentRelatedContext = currentRelatedContext(introspectedTable);
            interfaze.addImportedType(new FullyQualifiedJavaType(getModelName()));
        }
        return true;
    }

    @Override
    public boolean clientSelectByExampleWithoutBLOBsMethodGenerated(Method method, Interface interfaze, IntrospectedTable introspectedTable) {
        if (hasRelation(introspectedTable)) {
            interfaze.addMethod(generateClientSelectByExampleWithRelation(method, introspectedTable));
            Map<String, RelationContext> currentRelatedContext = currentRelatedContext(introspectedTable);
            interfaze.addImportedType(new FullyQualifiedJavaType(getModelName()));
        }
        return true;
    }

    @Override
    public boolean sqlMapDocumentGenerated(Document document,
                                           IntrospectedTable introspectedTable) {
        XmlElement parentElement = document.getRootElement();
        if (hasRelation(introspectedTable)) {
            parentElement.addElement(generateSqlMapSelectByPrimaryKeyWithRelation(introspectedTable));
            parentElement.addElement(generateSqlMapSelectByExampleWithRelation(introspectedTable));
            parentElement.addElement(generateSqlMapResultMapWithRelation(introspectedTable));
            parentElement.addElement(generateSqlMapRelationExampleWhereClauseWithRelation(introspectedTable));
        }
        return super.sqlMapDocumentGenerated(document, introspectedTable);
    }

    private  boolean hasRelation(IntrospectedTable introspectedTable) {
        return introspectedTable.getFullyQualifiedTable().getIntrospectedTableName().equals(tableName) && !currentRelatedContext(introspectedTable).isEmpty();
    }

    // Map[FieldName: RelationContext]
    private  Map<String, RelationContext> currentRelatedContext(IntrospectedTable introspectedTable) {
        Map<String, RelationContext> rTables = new HashMap<>();
        List<IntrospectedColumn> introspectedColumns = introspectedTable.getAllColumns();
        String tableName = introspectedTable.getFullyQualifiedTable().getIntrospectedTableName();
        for(int i = 0; i < introspectedColumns.size();i++) {
            IntrospectedColumn column = introspectedColumns.get(i);
            if (relationContexts.containsKey(column.getActualColumnName())) {
                RelationContext relatedInfo = relationContexts.get(column.getActualColumnName());
                if (tables.containsKey(relatedInfo.getTargetTableName())) {
                    relatedInfo.setTargetTable(tables.get(relatedInfo.getTargetTableName()));
                }
                rTables.put(column.getActualColumnName(), relatedInfo);
            }
        }
        return rTables;
    }

    private XmlElement generateSqlMapResultMapWithRelation(IntrospectedTable introspectedTable) {
        String tableName = introspectedTable.getFullyQualifiedTable().getIntrospectedTableName();
        XmlElement element = new XmlElement("resultMap");
        element.addAttribute(new Attribute("id", "ResultMapWithRelation"));
        element.addAttribute(new Attribute("type", modelName));
        List<IntrospectedColumn> introspectedColumns = introspectedTable.getAllColumns();
        List<IntrospectedColumn> primaryColumns = introspectedTable.getPrimaryKeyColumns();
        List<XmlElement> associations = new ArrayList<>();
        for(int i = 0; i < introspectedColumns.size();i++){
            IntrospectedColumn column = introspectedColumns.get(i);
            XmlElement columnElement = null;
            if (relationContexts.containsKey(column.getActualColumnName())) {
                RelationContext relationContext = relationContexts.get(column.getActualColumnName());
                columnElement = new XmlElement("association");
                columnElement.addAttribute(new Attribute("property", relationContext.getTargetFieldName()));
                columnElement.addAttribute(new Attribute("javaType", relationContext.getTargetFieldClass()));
                if (!tables.containsKey(relationContext.getTargetTableName())) {
                    throw new RuntimeException("related table not found");
                }
                IntrospectedTable relatedTable = tables.get(relationContext.getTargetTableName());
                String relatedTableName = relatedTable.getFullyQualifiedTable().getIntrospectedTableName();
                List<IntrospectedColumn> relatedColumns = relatedTable.getAllColumns();
                for (int j = 0; j < relatedColumns.size(); j++) {
                    IntrospectedColumn relatedColumn = relatedColumns.get(j);
                    XmlElement relatedColumnElement = null;
                    if (primaryColumns.contains(column)) {
                        relatedColumnElement = new XmlElement("id");
                    } else {
                        relatedColumnElement = new XmlElement("result");
                    }
                    relatedColumnElement.addAttribute(new Attribute("column", relatedTableName + "_" + relatedColumn.getActualColumnName()));
                    relatedColumnElement.addAttribute(new Attribute("jdbcType", relatedColumn.getJdbcTypeName()));
                    relatedColumnElement.addAttribute(new Attribute("property", relatedColumn.getJavaProperty()));
                    columnElement.addElement(relatedColumnElement);
                }
                associations.add(columnElement);
            } else {
                if (primaryColumns.contains(column)) {
                    columnElement = new XmlElement("id");
                } else {
                    columnElement = new XmlElement("result");
                }
                columnElement.addAttribute(new Attribute("column", tableName + "_" + column.getActualColumnName()));
                columnElement.addAttribute(new Attribute("jdbcType", column.getJdbcTypeName()));
                columnElement.addAttribute(new Attribute("property", column.getJavaProperty()));
                element.addElement(columnElement);
            }
        }
        for (XmlElement columnElement: associations) {
            element.addElement(columnElement);
        }
        context.getPlugins().sqlMapResultMapWithBLOBsElementGenerated(element, introspectedTable);
        return element;
    }
    private XmlElement generateSqlMapRelationExampleWhereClauseWithRelation(IntrospectedTable introspectedTable) {
        String tableName = introspectedTable.getFullyQualifiedTable().getIntrospectedTableName();
        XmlElement element = new XmlElement("sql");
        element.addAttribute(new Attribute("id", "Example_Where_Clause_With_Relation"));
        StringBuilder sb = new StringBuilder();
        sb.append("    <where >\n");
        sb.append("    <foreach collection=\"oredCriteria\" item=\"criteria\" separator=\"or\" >\n");
        sb.append("    <if test=\"criteria.valid\">\n");
        sb.append("    <trim prefix=\"(\" suffix=\")\" prefixOverrides=\"and\" >\n");
        sb.append("    <foreach collection=\"criteria.criteria\" item=\"criterion\" >\n");
        sb.append("    <choose >\n");
        sb.append("    <when test=\"criterion.noValue\" >\n");
        sb.append(String.format("    and %s.${criterion.condition}\n", tableName));
        sb.append("    </when>\n");
        sb.append("    <when test=\"criterion.singleValue\" >\n");
        sb.append(String.format("    and %s.${criterion.condition} #{criterion.value}\n", tableName));
        sb.append("    </when>\n");
        sb.append("    <when test=\"criterion.betweenValue\" >\n");
        sb.append(String.format("    and %s.${criterion.condition} #{criterion.value} and #{criterion.secondValue}\n", tableName));
        sb.append("    </when>\n");
        sb.append("    <when test=\"criterion.listValue\" >\n");
        sb.append(String.format("    and %s.${criterion.condition}\n", tableName));
        sb.append("    <foreach collection=\"criterion.value\" item=\"listItem\" open=\"(\" close=\")\" separator=\",\" >\n");
        sb.append("    #{listItem}\n");
        sb.append("    </foreach>\n");
        sb.append("    </when>\n");
        sb.append("    </choose>\n");
        sb.append("    </foreach>\n");
        sb.append("    </trim>\n");
        sb.append("    </if>\n");
        sb.append("    </foreach>\n");
        sb.append("    </where>");
        element.addElement(new TextElement(sb.toString()));
        return element;
    }

    private String selectClauseWithRelation(IntrospectedTable introspectedTable) {
        StringBuilder sb = new StringBuilder();
        boolean firstSelectColumn = true;
        for (IntrospectedColumn column: introspectedTable.getAllColumns()) {
            if (!firstSelectColumn)
                sb.append(String.format("    ,%s.%s as %s_%s\n", tableName, column.getActualColumnName(), tableName, column.getActualColumnName()));
            else
                sb.append(String.format("    %s.%s as %s_%s\n", tableName, column.getActualColumnName(), tableName, column.getActualColumnName()));
            firstSelectColumn = false;
        }
        Map<String, RelationContext> currentRelatedContext = currentRelatedContext(introspectedTable);
        for (Map.Entry<String, RelationContext> info: currentRelatedContext.entrySet()) {
            for (IntrospectedColumn column: info.getValue().getTargetTable().getAllColumns()) {
                sb.append(String.format("    ,%s.%s as %s_%s\n", info.getValue().getTargetTableName(), column.getActualColumnName(), info.getValue().getTargetTableName(), column.getActualColumnName()));
            }
        }
        sb.append("    from\n");
        sb.append("    " + tableName + "\n");
        for (RelationContext relationContext: currentRelatedContext.values()) {
            sb.append("    ," + relationContext.getTargetTableName());
            sb.append("\n");
        }
        return sb.toString();
    }

    private XmlElement generateSqlMapSelectByExampleWithRelation(IntrospectedTable introspectedTable) {
        String tableName = introspectedTable.getAliasedFullyQualifiedTableNameAtRuntime();// 鏁版嵁搴撹〃鍚
        XmlElement element = new XmlElement("select");
        element.addAttribute(new Attribute("id", "selectByExampleWithRelation"));
        element.addAttribute(new Attribute("parameterType", introspectedTable.getExampleType()));
        element.addAttribute(new Attribute("resultMap", "ResultMapWithRelation"));
        StringBuilder sb = new StringBuilder();
        sb.append("select <if test=\"distinct\"> distinct</if>\n");
        sb.append(selectClauseWithRelation(introspectedTable));
        Map<String, RelationContext> currentRelatedContext = currentRelatedContext(introspectedTable);
        sb.append("  <if test=\"_parameter != null\" >\n"
                + "    <include refid=\"Example_Where_Clause_With_Relation\" />\n"
                + "	   and\n"
                + "  </if>\n");
        sb.append("  <if test=\"_parameter == null\" > where </if>\n");
        boolean first = true;
        for (Map.Entry<String, RelationContext> info: currentRelatedContext.entrySet()) {
            if (!first) {
                sb.append("	   and\n");
            }
            sb.append(String.format("    %s.%s = %s.%s\n",
                    tableName,
                    info.getKey(),
                    info.getValue().getTargetTableName(),
                    info.getValue().getTargetTable().getPrimaryKeyColumns().get(0).getActualColumnName()));
            first = false;
        }
        sb.append("  <if test=\"orderByClause != null\" >\n"
                + "    order by ${orderByClause}\n"
                + "  </if>\n"
                + "  <if test=\"offset != null and offset>=0\" >\n"
                + "    limit #{offset} , #{size}\n"
                + "  </if>\n");
        element.addElement(new TextElement(sb.toString()));
        return element;
    }

    private XmlElement generateSqlMapSelectByPrimaryKeyWithRelation(IntrospectedTable introspectedTable) {
        String tableName = introspectedTable.getAliasedFullyQualifiedTableNameAtRuntime();// 鏁版嵁搴撹〃鍚
        XmlElement element = new XmlElement("select");
        element.addAttribute(new Attribute("id", "selectByPrimaryKeyWithRelation"));
        element.addAttribute(new Attribute("parameterType", introspectedTable.getPrimaryKeyColumns().get(0).getFullyQualifiedJavaType().getFullyQualifiedName()));
        element.addAttribute(new Attribute("resultMap", "ResultMapWithRelation"));
        StringBuilder sb = new StringBuilder();
        sb.append("select\n");
        sb.append(selectClauseWithRelation(introspectedTable));
        Map<String, RelationContext> currentRelatedContext = currentRelatedContext(introspectedTable);
        sb.append("    where\n");
        for (Map.Entry<String, RelationContext> info: currentRelatedContext.entrySet()) {
            sb.append(String.format("    %s.%s = %s.%s\n",
                    tableName,
                    info.getKey(),
                    info.getValue().getTargetTableName(),
                    info.getValue().getTargetTable().getPrimaryKeyColumns().get(0).getActualColumnName()));
            sb.append("    and\n");
        }
        sb.append(String.format("    %s.%s=#{%s}",
                tableName,
                introspectedTable.getPrimaryKeyColumns().get(0).getActualColumnName(),
                introspectedTable.getPrimaryKeyColumns().get(0).getJavaProperty()));
        element.addElement(new TextElement(sb.toString()));
        return element;
    }

    private GeneratedJavaFile generateModelFileWithRelation(IntrospectedTable introspectedTable) {
        RelationRecordGenerator generator = new RelationRecordGenerator();
        generator.setContext(context);
        generator.setIntrospectedTable(introspectedTable);
        generator.setRelationContexts(relationContexts);
        generator.setRelationModelClassName(modelName);
        CompilationUnit unit = generator.getCompilationUnits().get(0);
        return new GeneratedJavaFile(unit, context.getJavaClientGeneratorConfiguration()
                .getTargetProject(),
                context.getProperty(PropertyRegistry.CONTEXT_JAVA_FILE_ENCODING),
                context.getJavaFormatter());
    }

    private Method generateClientSelectByPrimaryKeyWithRelation(Method method, IntrospectedTable introspectedTable) {
        String tableName = introspectedTable.getFullyQualifiedTableNameAtRuntime();
        Method m = new Method("selectByPrimaryKeyWithRelation");
        m.setVisibility(method.getVisibility());
        m.setReturnType(new FullyQualifiedJavaType(modelName));
        for (Parameter p: method.getParameters()) {
            m.addParameter(p);
        }
        return m;
    }

    private Method generateClientSelectByExampleWithRelation(Method method, IntrospectedTable introspectedTable) {
        String tableName = introspectedTable.getFullyQualifiedTableNameAtRuntime();
        Method m = new Method("selectByExampleWithRelation");
        m.setVisibility(method.getVisibility());
        m.setReturnType(new FullyQualifiedJavaType(String.format("java.util.List<%s>", modelName)));
        for (Parameter p: method.getParameters()) {
            m.addParameter(p);
        }
        return m;
    }

    @Override
    public boolean validate(List<String> warnings) {
        return true;
    }

    @Data
    static public class RelationRecordGenerator extends BaseRecordGenerator {

        private String relationModelClassName;
        private Map<String, RelationContext> relationContexts = new HashMap<>();

        @Override
        public List<CompilationUnit> getCompilationUnits() {
            FullyQualifiedTable table = introspectedTable.getFullyQualifiedTable();
            Plugin plugins = context.getPlugins();
            CommentGenerator commentGenerator = context.getCommentGenerator();

            FullyQualifiedJavaType type = new FullyQualifiedJavaType(relationModelClassName);
            TopLevelClass topLevelClass = new TopLevelClass(type);
            topLevelClass.setVisibility(JavaVisibility.PUBLIC);
            commentGenerator.addJavaFileComment(topLevelClass);

            FullyQualifiedJavaType superClass = getSuperClass();
            if (superClass != null) {
                topLevelClass.setSuperClass(superClass);
                topLevelClass.addImportedType(superClass);
            }
            commentGenerator.addClassComment(topLevelClass, introspectedTable);

            List<IntrospectedColumn> introspectedColumns = getColumnsInThisClass();
            if (introspectedTable.isConstructorBased()) {
                addParameterizedConstructor(topLevelClass);
                if (!introspectedTable.isImmutable()) {
                    addDefaultConstructor(topLevelClass);
                }
            }
            String rootClass = getRootClass();
            topLevelClass.addAnnotation("@lombok.Data");
            for (IntrospectedColumn introspectedColumn : introspectedColumns) {
                if (RootClassInfo.getInstance(rootClass, warnings).containsProperty(introspectedColumn)) {
                    continue;
                }
                Field field = getJavaBeansField(introspectedColumn);
                if (this.relationContexts.containsKey(introspectedColumn.getActualColumnName())) {
                    field = getRelationField(relationContexts.get(introspectedColumn.getActualColumnName()));
                    topLevelClass.addField(field);
                    topLevelClass.addImportedType(field.getType());
                } else if (plugins.modelFieldGenerated(field, topLevelClass,
                        introspectedColumn, introspectedTable,
                        Plugin.ModelClassType.BASE_RECORD)) {
                    topLevelClass.addField(field);
                    topLevelClass.addImportedType(field.getType());
                }
            }
            plugins.modelBaseRecordClassGenerated(topLevelClass, introspectedTable);
            List<CompilationUnit> answer = new ArrayList<CompilationUnit>();
            answer.add(topLevelClass);
            return answer;
        }

        private FullyQualifiedJavaType getSuperClass() {
            FullyQualifiedJavaType superClass;
            if (introspectedTable.getRules().generatePrimaryKeyClass()) {
                superClass = new FullyQualifiedJavaType(introspectedTable
                        .getPrimaryKeyType());
            } else {
                String rootClass = getRootClass();
                if (rootClass != null) {
                    superClass = new FullyQualifiedJavaType(rootClass);
                } else {
                    superClass = null;
                }
            }

            return superClass;
        }

        private Field getRelationField(RelationContext context) {
            Field field = new Field();
            field.setVisibility(JavaVisibility.PRIVATE);
            field.setType(new FullyQualifiedJavaType(context.getTargetFieldClass()));
            field.setName(context.getTargetFieldName());
            return field;
        }

        private boolean includePrimaryKeyColumns() {
            return !introspectedTable.getRules().generatePrimaryKeyClass()
                    && introspectedTable.hasPrimaryKeyColumns();
        }

        private boolean includeBLOBColumns() {
            return !introspectedTable.getRules().generateRecordWithBLOBsClass()
                    && introspectedTable.hasBLOBColumns();
        }

        private void addParameterizedConstructor(TopLevelClass topLevelClass) {
            Method method = new Method();
            method.setVisibility(JavaVisibility.PUBLIC);
            method.setConstructor(true);
            method.setName(topLevelClass.getType().getShortName());
            context.getCommentGenerator().addGeneralMethodComment(method, introspectedTable);

            List<IntrospectedColumn> constructorColumns =
                    includeBLOBColumns() ? introspectedTable.getAllColumns() :
                            introspectedTable.getNonBLOBColumns();

            for (IntrospectedColumn introspectedColumn : constructorColumns) {
                method.addParameter(new Parameter(introspectedColumn.getFullyQualifiedJavaType(),
                        introspectedColumn.getJavaProperty()));
                topLevelClass.addImportedType(introspectedColumn.getFullyQualifiedJavaType());
            }

            StringBuilder sb = new StringBuilder();
            if (introspectedTable.getRules().generatePrimaryKeyClass()) {
                boolean comma = false;
                sb.append("super("); //$NON-NLS-1$
                for (IntrospectedColumn introspectedColumn : introspectedTable
                        .getPrimaryKeyColumns()) {
                    if (comma) {
                        sb.append(", "); //$NON-NLS-1$
                    } else {
                        comma = true;
                    }
                    sb.append(introspectedColumn.getJavaProperty());
                }
                sb.append(");"); //$NON-NLS-1$
                method.addBodyLine(sb.toString());
            }

            List<IntrospectedColumn> introspectedColumns = getColumnsInThisClass();

            for (IntrospectedColumn introspectedColumn : introspectedColumns) {
                sb.setLength(0);
                sb.append("this."); //$NON-NLS-1$
                sb.append(introspectedColumn.getJavaProperty());
                sb.append(" = "); //$NON-NLS-1$
                sb.append(introspectedColumn.getJavaProperty());
                sb.append(';');
                method.addBodyLine(sb.toString());
            }

            topLevelClass.addMethod(method);
        }

        private List<IntrospectedColumn> getColumnsInThisClass() {
            List<IntrospectedColumn> introspectedColumns;
            if (includePrimaryKeyColumns()) {
                if (includeBLOBColumns()) {
                    introspectedColumns = introspectedTable.getAllColumns();
                } else {
                    introspectedColumns = introspectedTable.getNonBLOBColumns();
                }
            } else {
                if (includeBLOBColumns()) {
                    introspectedColumns = introspectedTable
                            .getNonPrimaryKeyColumns();
                } else {
                    introspectedColumns = introspectedTable.getBaseColumns();
                }
            }
            return introspectedColumns;
        }
    }
}
